package org.juiceshop.musi.midi;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.TreeSet;


public class MidiSequence extends TreeSet<MidiSequence.Event> {
	
	private static final long serialVersionUID = 1652881317421826231L;

	enum EventType {
		END_OF_TRACK(0x2f),
		SET_TEMPO(0x51),
		TIME_SIGNATURE(0x58),
		KEY_SIGNATURE(0x59),
		NOTE_OFF(0x8),
		NOTE_ON(0x9),
		NOTE_AFTERTOUCH(0xA),
		CONTROLLER(0xB),
		PROGRAM_CHANGE(0xC),
		CHANNEL_AFTERTOUCH(0xD),
		PITCH_BEND(0xE);
		private int _number;
		private EventType(int number) {
			_number = number;
		}
		public int getNumber() {
			return _number;
		}
		public boolean isChannelEvent() {
			return _number < 0xF;
		}
		public boolean isMeta() {
			return !isChannelEvent();
		}
//		public EventType fromNumber(int number) {
//			for (EventType et : values()) {
//				if (et._number==number) return et;
//			}
//			return null;
//		}
	}



	private int _resolution;
	private int _count;

	public MidiSequence(int resolution) {
		super();
		_resolution = resolution;
	}
	
	public int getResolution() {
		return _resolution;
	}
	
	public void addNoteOffEvent(long tick, int channel, int pitch, int velocity) {
		add(new ChannelEvent(tick,EventType.NOTE_OFF,channel,pitch, velocity));
	}
	
	public void addNoteOnEvent(long tick, int channel, int pitch, int velocity) {
		add(new ChannelEvent(tick,EventType.NOTE_ON,channel,pitch,velocity));
	}
	
	public void addNoteAftertouchEvent(long tick, int channel, int pitch, int value) {
		add (new ChannelEvent(tick,EventType.NOTE_AFTERTOUCH,channel,pitch,value));
	}
	
	public void addControllerEvent(long tick, int channel, int controller, int value) {
		add (new ChannelEvent(tick, EventType.CONTROLLER, channel, controller, value));
	}
	
	public void addProgramChangeEvent(long tick, int channel, int programNumber) {
		add (new ChannelEvent(tick,EventType.PROGRAM_CHANGE,channel,programNumber,0));
	}

	public void addChannelAftertouchEvent(long tick, int channel, int value) {
		add (new ChannelEvent(tick,EventType.CHANNEL_AFTERTOUCH,channel,value,0));
	}
	
	public void addPitchBendEvent(long tick, int channel, int value) {
		add (new ChannelEvent(tick,EventType.PITCH_BEND,channel,value&0xFF,value>>8));
	}
	
	public void addEndOfTrackEvent(long tick) {
		add (new MetaEvent(tick,EventType.END_OF_TRACK));
	}
	
	public void addTempoEvent(long tick, long bpm) {
		long mpqn = 60000000 / bpm;
		add (new MetaEvent(tick,EventType.SET_TEMPO,
			(byte)((mpqn>>16) & 0xFF),
			(byte)((mpqn>>8) & 0xFF),
			(byte)(mpqn & 0xFF)
		));
	}
	

	public void write(OutputStream os) throws IOException{
		MidiOutputStream mos = new MidiOutputStream(os);
		mos.writeHeader(_resolution,1);
		
		//write Track
		//Write to temporary stream to buffer disk writes and
		//calculate the number of bytes written to the stream
		MidiOutputStream tempMos = new MidiOutputStream();
		long lastTick = 0;
		for (Event evt : this) {
			long tick = evt.tick;
			tempMos.writeVariable((int)(tick-lastTick));
			lastTick = tick;
			evt.writeBytes(tempMos);
		}
		//Write to the real stream
		mos.writeChunk(tempMos);
		mos.close();
	}
	
	
	public byte[] toByteArray() {
		ByteArrayOutputStream out = new ByteArrayOutputStream();
		try {
			write(out);
		} catch (IOException e) {
			throw new Error(e);
		}
		return out.toByteArray();
	}
	
	
	@Override
	public String toString() {
		return "MidiSequence resolution="+_resolution;
	}

	public void print() {
		System.out.println(this);
		for (Event ev : this) {
			System.out.println(ev);
		}
	}
	
	public abstract class Event implements Comparable<Event>
	{
		long tick;
		int seq;
		private Event(long tick) {
			this.tick = tick;
			this.seq = _count++;
		}
		@Override
		public int compareTo(Event o) {
			return tick<o.tick ? -1 : tick>o.tick ? 1 :
				seq<o.seq ? -1 : seq>o.seq ? 1 : 0 ;
		}
		abstract void writeBytes(MidiOutputStream out) throws IOException;
		@Override
		public String toString() {
			return tick+"("+seq+"): "+this.getClass().getSimpleName()+" ";
		}
	}
	
	public class ChannelEvent extends Event {
		EventType type;
		int channel;
		int param1;
		int param2;
		private ChannelEvent(long tick, EventType type, int channel, int param1, int param2) {
			super(tick);
			this.type = type;
			this.channel = channel;
			this.param1 = param1;
			this.param2 = param2;
		}
		@Override
		void writeBytes(MidiOutputStream out) throws IOException {
			out.writeBytes((type.getNumber()<<4) | channel, param1, param2);
		}
		@Override
		public String toString() {
			return super.toString()+"eventType="+type+" Channel="+channel+" params="+param1+","+param2;
		}
	}
	
	public class MetaEvent extends Event {
		EventType type;
		byte[] data;
		private MetaEvent (long tick, EventType type, byte...data) {
			super(tick);
			this.type = type;
			this.data = data;
		}
		@Override
		void writeBytes(MidiOutputStream out) throws IOException {
			out.writeBytes(0xFF,type.getNumber());
			out.writeVariable(data.length);
			out.write(data);
		}
		@Override
		public String toString() {
			return super.toString()+"type="+type+" data="+Arrays.toString(data);
		}
	}

}
